package eu.expressis.eiaf.core.infrastructure.integration.db.dao;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceException;
import javax.persistence.Query;

import org.springframework.dao.DataAccessException;
import org.springframework.orm.jpa.JpaCallback;
import org.springframework.util.CollectionUtils;

import eu.expressis.eiaf.core.infrastructure.integration.db.entity.AbstractEntity;

public class JpaDao<E extends AbstractEntity> extends AbstractDefaultJpaDao<E> {

  private static final String AND = " AND ";
  private static final String WHERE = " WHERE ";

  protected Class<E> entityClass;
  private boolean useQueryCache = false;

  protected JpaDao() {
    super();
  }

  protected JpaDao(Class<E> entityClass) {
    super();
    this.entityClass = entityClass;
  }

  /**
   * Find all BaseObject entities with a specific property value.
   * 
   * @param propertyName the name of the BaseObject property to query
   * @param value the property value to match
   * @return List<E> found by query
   */
  @SuppressWarnings("unchecked")
  public List<E> findByProperty(String propertyName, final Object value) {
    try {
      final Map<String, Object> params = new HashMap<String, Object>();
      params.put(propertyName, value);
      String queryString = new StringBuilder("select model from ").append(getEntityClass().getSimpleName())
          .append(" model where model.").append(propertyName).append("= :").append(propertyName).toString();
      final List<E> resultList = findByNamedParams(queryString, params);
      return CollectionUtils.isEmpty(resultList) ? new ArrayList<E>() : resultList;
    } catch (RuntimeException re) {
      throw re;
    }
  }

  /**
   * Find all BaseObject entities with a specific property value.
   * 
   * @param properties the properties used to filter the result
   * @return List<E> found by query
   */

  public List<E> findByProperties(java.util.Map<String, Object> properties) {
    return findByProperties(properties, null);
  }

  /**
   * Find all AbstractPersistentEntity entities with the filtered with the values specified with properties.
   * 
   * @param properties the properties used to filter the result
   * @param orderBy the fields to order the result
   * @return List<E> found by query
   */

  @SuppressWarnings("unchecked")
  public List<E> findByProperties(java.util.Map<String, Object> properties, List<String> orderBy) {
    try {
      final Map<String, Object> parms = new HashMap<String, Object>();
      Set<String> keys = properties.keySet();
      StringBuilder queryString = new StringBuilder("SELECT model FROM " + getEntityClass().getSimpleName()
          + " model WHERE ");
      for(String key : keys) {
        int firstIndex = key.indexOf(".");
        String keyParm = (firstIndex > 0) ? key.substring(key.lastIndexOf(".") + 1, key.length()) : key;
        queryString.append("model.").append(key).append("= :").append(keyParm).append(AND);
        parms.put(keyParm, properties.get(key));
      }
      if (queryString.toString().endsWith(AND))
        queryString.delete(queryString.lastIndexOf(AND), queryString.length());

      if ((orderBy != null) && (orderBy.size() > 0)) {
        queryString.append(" ORDER BY model." + orderBy.get(0));

        for(int i = 1; i < orderBy.size(); ++i)
          queryString.append(", model." + orderBy.get(i));
      }
      final List<E> resultList = findByNamedParams(queryString.toString(), parms);
      return CollectionUtils.isEmpty(resultList) ? new ArrayList<E>() : resultList;
    } catch (RuntimeException re) {
      throw re;
    }
  }

  public E save(E object) {
    if (getJpaTemplate().contains(object)) {
      getJpaTemplate().merge(object);
    } else {
      getJpaTemplate().persist(object);
    }
    getJpaTemplate().flush();
    getJpaTemplate().refresh(object);
    return object;
  }

  @SuppressWarnings("unchecked")
  public List<E> findByQueryString(String queryString) {
    return find(queryString);
  }

  public <T> T update(final T entity) throws DataAccessException {
    return getJpaTemplate().merge(entity);
  }

  @Override
  protected Class<?> getEntityClass() {
    return entityClass != null ? entityClass : super.getEntityClass();
  }

  public void flush() {
    getJpaTemplate().flush();
  }

  @SuppressWarnings("rawtypes")
  public List findBySqlQuery(final String sqlString) throws DataAccessException {
    return getJpaTemplate().executeFind(new JpaCallback() {
      public Object doInJpa(EntityManager em) throws PersistenceException {
        Query queryObject = em.createNativeQuery(sqlString);
        final List resultList = queryObject.getResultList();
        return CollectionUtils.isEmpty(resultList) ? new ArrayList() : resultList;
      }
    });
  }

  @SuppressWarnings("unchecked")
  public <T> List<T> findByNativeSqlQuery(final String sqlString, final Class<T> resultClass)
      throws DataAccessException {
    return getJpaTemplate().executeFind(new JpaCallback() {
      public Object doInJpa(EntityManager em) throws PersistenceException {
        Query queryObject = em.createNativeQuery(sqlString, resultClass);
        final List<T> resultList = queryObject.getResultList();
        return CollectionUtils.isEmpty(resultList) ? new ArrayList<T>() : resultList;
      }
    });
  }

  @SuppressWarnings("rawtypes")
  public List find(String queryString) throws DataAccessException {
    return find(queryString, (Object[]) null);
  }

  @SuppressWarnings("rawtypes")
  public List find(final String queryString, final Object... values) throws DataAccessException {
    return getJpaTemplate().executeFind(new JpaCallback() {
      public Object doInJpa(EntityManager em) throws PersistenceException {
        Query queryObject = em.createQuery(queryString);
        if (values != null) {
          for(int i = 0; i < values.length; i++) {
            queryObject.setParameter(i + 1, values[i]);
          }
        }
        queryObject.setHint("org.hibernate.cacheable", useQueryCache);
        final List resultList = queryObject.getResultList();
        return CollectionUtils.isEmpty(resultList) ? new ArrayList() : resultList;
      }
    });
  }

  @SuppressWarnings("rawtypes")
  public List findByNamedParams(final String queryString, final Map<String, ?> params) throws DataAccessException {
    return getJpaTemplate().executeFind(new JpaCallback() {
      public Object doInJpa(EntityManager em) throws PersistenceException {
        Query queryObject = em.createQuery(queryString);
        if (params != null) {
          for(Map.Entry<String, ?> entry : params.entrySet()) {
            queryObject.setParameter(entry.getKey(), entry.getValue());
          }
        }
        queryObject.setHint("org.hibernate.cacheable", useQueryCache);
        final List resultList = queryObject.getResultList();
        return CollectionUtils.isEmpty(resultList) ? new ArrayList() : resultList;
      }
    });
  }

  @SuppressWarnings("rawtypes")
  public List findByNamedQuery(String queryName) throws DataAccessException {
    return findByNamedQuery(queryName, (Object[]) null);
  }

  @SuppressWarnings("rawtypes")
  public List findByNamedQuery(final String queryName, final Object... values) throws DataAccessException {
    return getJpaTemplate().executeFind(new JpaCallback() {
      public Object doInJpa(EntityManager em) throws PersistenceException {
        Query queryObject = em.createNamedQuery(queryName);
        if (values != null) {
          for(int i = 0; i < values.length; i++) {
            queryObject.setParameter(i + 1, values[i]);
          }
        }
        queryObject.setHint("org.hibernate.cacheable", useQueryCache);
        final List resultList = queryObject.getResultList();
        return CollectionUtils.isEmpty(resultList) ? new ArrayList() : resultList;
      }
    });
  }

  public Long getEntitiesCount(final String queryString, final Map<String, ?> params) throws DataAccessException {
    return (Long) getJpaTemplate().execute(new JpaCallback() {
      public Object doInJpa(EntityManager em) throws PersistenceException {
        Query queryObject = em.createQuery(queryString);
        if (params != null) {
          for(Map.Entry<String, ?> entry : params.entrySet()) {
            queryObject.setParameter(entry.getKey(), entry.getValue());
          }
        }
        queryObject.setHint("org.hibernate.cacheable", useQueryCache);
        return queryObject.getSingleResult();
      }
    });
  }

  @SuppressWarnings("rawtypes")
  public List findByNamedQueryAndNamedParams(final String queryName, final Map<String, ?> params)
      throws DataAccessException {

    return getJpaTemplate().executeFind(new JpaCallback() {
      public Object doInJpa(EntityManager em) throws PersistenceException {
        Query queryObject = em.createNamedQuery(queryName);
        if (params != null) {
          for(Map.Entry<String, ?> entry : params.entrySet()) {
            queryObject.setParameter(entry.getKey(), entry.getValue());
          }
        }
        queryObject.setHint("org.hibernate.cacheable", useQueryCache);
        final List resultList = queryObject.getResultList();
        return CollectionUtils.isEmpty(resultList) ? new ArrayList() : resultList;
      }
    });
  }

  public int executeNativeUpdate(final String queryString, final Map<String, ?> params) throws DataAccessException {
    // Hibernate bugs on bulk delete/update
    // see http://opensource.atlassian.com/projects/hibernate/browse/HHH-1657
    // so use native query
    EntityManager em = getJpaTemplate().getEntityManagerFactory().createEntityManager();
    Query query = em.createNativeQuery(queryString);
    if (params != null) {
      for(Map.Entry<String, ?> entry : params.entrySet()) {
        query.setParameter(entry.getKey(), entry.getValue());
      }
    }
    return query.executeUpdate();
  }

  public Integer executeUpdate(final String queryString, final Map<String, ?> params) throws DataAccessException {
    return (Integer) getJpaTemplate().execute(new JpaCallback() {
      public Object doInJpa(EntityManager em) throws PersistenceException {
        Query queryObject = em.createQuery(queryString);
        if (params != null) {
          for(Map.Entry<String, ?> entry : params.entrySet()) {
            queryObject.setParameter(entry.getKey(), entry.getValue());
          }
        }
        return Integer.valueOf(queryObject.executeUpdate());
      }
    });
  }

  public void setUseQueryCache(boolean useQueryCache) {
    this.useQueryCache = useQueryCache;
  }

  /**
   * if at least one element before ... need an and
   * 
   * @param params
   * @return "" or " and " string
   */
  public String getAndString(Map<?, ?> params) {
    return params.isEmpty() ? "" : AND;
  }

  /**
   * Adds a logicalBinding in if this is not the first search criteria
   * 
   * @param sql
   * @param firstCriteria
   */
  public boolean addLogicalBindingOrWhereClause(StringBuilder sql, boolean firstCriteria) {
    if (firstCriteria) {
      sql.append(AND);
    } else {
      sql.append(WHERE);
    }
    return true;
  }
}